package io.noties.markwon.image;

import com.noties.markwon.annotation.NonNull;
import com.noties.markwon.annotation.Nullable;
import com.noties.markwon.wrapper.graphics.drawable.Drawable;
import com.noties.markwon.wrapper.text.Spanned;
import java.util.HashMap;
import ohos.agp.components.Component;
import ohos.agp.components.Text;
import ohos.agp.utils.Rect;
import ohos.eventhandler.EventRunner;
import ohos.miscservices.timeutility.Time;

public abstract class AsyncDrawableScheduler {
    private static final Integer MARKWON_DRAWABLES_SCHEDULER_LAST_TEXT_HASHCODE = 0x0001;
    private static final Integer MARKWON_DRAWABLES_SCHEDULER = 0x0002;

    public static void schedule(@NonNull final Text textView) {

        // we need a simple check if current text has already scheduled drawables
        // we need this in order to allow multiple calls to schedule (different plugins
        // might use AsyncDrawable), but we do not want to repeat the task
        //
        // hm... we need the same thing for unschedule then... we can check if last hash is !null,
        // if it's not -> unschedule, else ignore

        // @since 4.0.0
        // TODO R.id
        HashMap<Integer, Object> tagMap = (HashMap<Integer, Object>)textView.getTag();
        if (tagMap == null) {
            tagMap = new HashMap<>();
            textView.setTag(tagMap);
        }
        final Integer lastTextHashCode = (Integer) tagMap.get(MARKWON_DRAWABLES_SCHEDULER_LAST_TEXT_HASHCODE);
        final int textHashCode = textView.getText().hashCode();
        if (lastTextHashCode != null
                && lastTextHashCode == textHashCode) {
            return;
        }
        tagMap.put(MARKWON_DRAWABLES_SCHEDULER_LAST_TEXT_HASHCODE, textHashCode);
        textView.setTag(tagMap);


        final AsyncDrawableSpan[] spans = extractSpans(textView);
        if (spans != null
                && spans.length > 0) {
            tagMap = (HashMap<Integer, Object>)textView.getTag();
            if (tagMap.get(MARKWON_DRAWABLES_SCHEDULER) == null) {
                HashMap<Integer, Object> finalTagMap = tagMap;
                final Component.BindStateChangedListener listener = new Component.BindStateChangedListener() {

                    @Override
                    public void onComponentBoundToWindow(Component component) {

                    }

                    @Override
                    public void onComponentUnboundFromWindow(Component component) {
                        unschedule(textView);
                        component.removeBindStateChangedListener(this);
                        finalTagMap.put(MARKWON_DRAWABLES_SCHEDULER, null);
                        component.setTag(finalTagMap);
                    }
                };
                textView.setBindStateChangedListener(listener);
                finalTagMap.put(MARKWON_DRAWABLES_SCHEDULER, listener);
                textView.setTag(finalTagMap);
            }

            // @since 4.1.0
            final DrawableCallbackImpl.Invalidator invalidator = new TextViewInvalidator(textView);

            AsyncDrawable drawable;

            for (AsyncDrawableSpan span : spans) {
                drawable = span.getDrawable();
                drawable.setCallback2(new DrawableCallbackImpl(textView, invalidator, drawable.getBounds()));
            }
        }
    }

    // must be called when text manually changed in Text
    public static void unschedule(@NonNull Text view) {

        // @since 4.0.0
        HashMap<Integer, Object> tagMap = (HashMap<Integer, Object>)view.getTag();
        if (tagMap == null) {
            tagMap = new HashMap<>();
            view.setTag(tagMap);
        }
        if (tagMap.get(MARKWON_DRAWABLES_SCHEDULER_LAST_TEXT_HASHCODE) == null) {
            return;
        }
        tagMap.put(MARKWON_DRAWABLES_SCHEDULER_LAST_TEXT_HASHCODE, null);
        view.setTag(tagMap);


        final AsyncDrawableSpan[] spans = extractSpans(view);
        if (spans != null
                && spans.length > 0) {
            for (AsyncDrawableSpan span : spans) {
                span.getDrawable().setCallback2(null);
            }
        }
    }

    @Nullable
    private static AsyncDrawableSpan[] extractSpans(@NonNull Text textView) {

        final CharSequence cs = textView.getText();
        final int length = cs != null
                ? cs.length()
                : 0;

        if (length == 0
                || !(cs instanceof Spanned)) {
            return null;
        }

        // we also could've tried the `nextSpanTransition`, but strangely it leads to worse performance
        // than direct getSpans

        return ((Spanned) cs).getSpans(0, length, AsyncDrawableSpan.class);
    }

    private AsyncDrawableScheduler() {
    }

    private static class DrawableCallbackImpl implements Drawable.Callback {

        // @since 4.1.0
        // interface to be used when bounds change and view must be invalidated
        interface Invalidator {
            void invalidate();
        }

        private final Text view;
        private final Invalidator invalidator; // @since 4.1.0

        private Rect previousBounds;

        DrawableCallbackImpl(
                @NonNull Text view,
                @NonNull Invalidator invalidator,
                Rect initialBounds) {
            this.view = view;
            this.invalidator = invalidator;
            this.previousBounds = new Rect(initialBounds);
        }

        @Override
        public void invalidateDrawable(@NonNull final Drawable who) {

            if (EventRunner.current() != EventRunner.getMainEventRunner()) {
                // TODO
//                view.post(new Runnable() {
//                    @Override
//                    public void run() {
//                        invalidateDrawable(who);
//                    }
//                });
                return;
            }

            final Rect rect = who.getBounds();

            // okay... the thing is IF we do not change bounds size, normal invalidate would do
            // but if the size has changed, then we need to update the whole layout...

            if (!previousBounds.equals(rect)) {
                // @since 4.1.0
                // invalidation moved to upper level (so invalidation can be deferred,
                // and multiple calls combined)
                invalidator.invalidate();
                previousBounds = new Rect(rect);
            } else {

                view.invalidate();
            }
        }

        @Override
        public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
            final long delay = when - Time.getRealActiveTime();
            // TODO
//            view.postDelayed(what, delay);
        }

        @Override
        public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
            // TODO
//            view.removeCallbacks(what);
        }
    }

    private static class TextViewInvalidator implements DrawableCallbackImpl.Invalidator, Runnable {

        private final Text textView;

        TextViewInvalidator(@NonNull Text textView) {
            this.textView = textView;
        }

        @Override
        public void invalidate() {
            // TODO
//            textView.removeCallbacks(this);
//            textView.post(this);
        }

        @Override
        public void run() {
            textView.setText(textView.getText());
        }
    }
}
